import Foundation
import CoreLocation
import SwiftSignalKit
import StoreKit
import TelegramCore
import Postbox
import TelegramStringFormatting
import TelegramUIPreferences
import PersistentStringHash

private let productIdentifiers = [
    "org.telegram.telegramPremium.annual",
    "org.telegram.telegramPremium.semiannual",
    "org.telegram.telegramPremium.monthly",
    "org.telegram.telegramPremium.twelveMonths",
    "org.telegram.telegramPremium.sixMonths",
    "org.telegram.telegramPremium.threeMonths",

    "org.telegram.telegramPremium.threeMonths.code_x1",
    "org.telegram.telegramPremium.sixMonths.code_x1",
    "org.telegram.telegramPremium.twelveMonths.code_x1",
    
    "org.telegram.telegramPremium.threeMonths.code_x5",
    "org.telegram.telegramPremium.sixMonths.code_x5",
    "org.telegram.telegramPremium.twelveMonths.code_x5",
    
    "org.telegram.telegramPremium.threeMonths.code_x10",
    "org.telegram.telegramPremium.sixMonths.code_x10",
    "org.telegram.telegramPremium.twelveMonths.code_x10",
    
    "org.telegram.telegramPremium.oneWeek.auth",
    
    "org.telegram.telegramStars.topup.x15",
    "org.telegram.telegramStars.topup.x25",
    "org.telegram.telegramStars.topup.x50",
    "org.telegram.telegramStars.topup.x75",
    "org.telegram.telegramStars.topup.x100",
    "org.telegram.telegramStars.topup.x150",
    "org.telegram.telegramStars.topup.x250",
    "org.telegram.telegramStars.topup.x350",
    "org.telegram.telegramStars.topup.x500",
    "org.telegram.telegramStars.topup.x750",
    "org.telegram.telegramStars.topup.x1000",
    "org.telegram.telegramStars.topup.x1500",
    "org.telegram.telegramStars.topup.x2500",
    "org.telegram.telegramStars.topup.x5000",
    "org.telegram.telegramStars.topup.x10000",
    "org.telegram.telegramStars.topup.x25000",
    "org.telegram.telegramStars.topup.x35000"
]

private extension NSDecimalNumber {
    func round(_ decimals: Int) -> NSDecimalNumber {
        return self.rounding(accordingToBehavior:
                            NSDecimalNumberHandler(roundingMode: .down,
                                   scale: Int16(decimals),
                                   raiseOnExactness: false,
                                   raiseOnOverflow: false,
                                   raiseOnUnderflow: false,
                                   raiseOnDivideByZero: false))
    }
    
    func prettyPrice() -> NSDecimalNumber {
        return self.multiplying(by: NSDecimalNumber(value: 2))
            .rounding(accordingToBehavior:
                NSDecimalNumberHandler(
                    roundingMode: .plain,
                    scale: Int16(0),
                    raiseOnExactness: false,
                    raiseOnOverflow: false,
                    raiseOnUnderflow: false,
                    raiseOnDivideByZero: false
                )
            )
            .dividing(by: NSDecimalNumber(value: 2))
            .subtracting(NSDecimalNumber(value: 0.01))
    }
}

public final class InAppPurchaseManager: NSObject {
    public final class Product: Equatable {
        private lazy var numberFormatter: NumberFormatter = {
            let numberFormatter = NumberFormatter()
            numberFormatter.numberStyle = .currency
            numberFormatter.locale = self.skProduct.priceLocale
            return numberFormatter
        }()
        
        let skProduct: SKProduct
        
        init(skProduct: SKProduct) {
            self.skProduct = skProduct
        }
        
        public var id: String {
            return self.skProduct.productIdentifier
        }
        
        public var isSubscription: Bool {
            if #available(iOS 12.0, *) {
                return self.skProduct.subscriptionGroupIdentifier != nil
            } else {
                return self.skProduct.subscriptionPeriod != nil
            }
        }
        
        public var price: String {
            return self.numberFormatter.string(from: self.skProduct.price) ?? ""
        }
        
        public func pricePerMonth(_ monthsCount: Int) -> String {
            let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2)
            return self.numberFormatter.string(from: price) ?? ""
        }
        
        public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String {
            let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2)
            let prettierPrice = price
                .multiplying(by: NSDecimalNumber(value: 2))
                .rounding(accordingToBehavior:
                    NSDecimalNumberHandler(
                        roundingMode: .up,
                        scale: Int16(0),
                        raiseOnExactness: false,
                        raiseOnOverflow: false,
                        raiseOnUnderflow: false,
                        raiseOnDivideByZero: false
                    )
                )
                .dividing(by: NSDecimalNumber(value: 2))
                .subtracting(NSDecimalNumber(value: 0.01))
            return self.numberFormatter.string(from: prettierPrice) ?? ""
        }
        
        public func multipliedPrice(count: Int) -> String {
            let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2)
            let prettierPrice = price
                .multiplying(by: NSDecimalNumber(value: 2))
                .rounding(accordingToBehavior:
                    NSDecimalNumberHandler(
                        roundingMode: .up,
                        scale: Int16(0),
                        raiseOnExactness: false,
                        raiseOnOverflow: false,
                        raiseOnUnderflow: false,
                        raiseOnDivideByZero: false
                    )
                )
                .dividing(by: NSDecimalNumber(value: 2))
                .subtracting(NSDecimalNumber(value: 0.01))
            return self.numberFormatter.string(from: prettierPrice) ?? ""
        }
        
        public var priceValue: NSDecimalNumber {
            return self.skProduct.price
        }
        
        public var priceCurrencyAndAmount: (currency: String, amount: Int64) {
            if let currencyCode = self.numberFormatter.currencyCode,
                let amount = fractionalToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) {
                return (currencyCode, amount)
            } else {
                return ("", 0)
            }
        }
        
        public static func ==(lhs: Product, rhs: Product) -> Bool {
            if lhs.id != rhs.id {
                return false
            }
            if lhs.isSubscription != rhs.isSubscription {
                return false
            }
            if lhs.priceValue != rhs.priceValue {
                return false
            }
            return true
        }
        
    }
    
    public enum PurchaseState {
        case purchased(transactionId: String)
    }
    
    public enum PurchaseError {
        case generic
        case cancelled
        case network
        case notAllowed
        case cantMakePayments
        case assignFailed
        case tryLater
    }
    
    public enum RestoreState {
        case succeed(Bool)
        case failed
    }
    
    private final class PaymentTransactionContext {
        var state: SKPaymentTransactionState?
        let purpose: PendingInAppPurchaseState.Purpose
        let subscriber: (TransactionState) -> Void
        
        init(purpose: PendingInAppPurchaseState.Purpose, subscriber: @escaping (TransactionState) -> Void) {
            self.purpose = purpose
            self.subscriber = subscriber
        }
    }
    
    private enum TransactionState {
        case purchased(transactionId: String?)
        case restored(transactionId: String?)
        case purchasing
        case failed(error: SKError?)
        case assignFailed
        case deferred
    }
    
    private let engine: SomeTelegramEngine
    
    private var products: [Product] = []
    private var productsPromise = Promise<[Product]>([])
    private var productRequest: SKProductsRequest?
    
    private let stateQueue = Queue()
    private var paymentContexts: [String: PaymentTransactionContext] = [:]
    
    private var finishedSuccessfulTransactions = Set<String>()
        
    private var onRestoreCompletion: ((RestoreState) -> Void)?
    
    private let disposableSet = DisposableDict<String>()
    
    private var lastRequestTimestamp: Double?

    public init(engine: SomeTelegramEngine) {
        self.engine = engine
                
        super.init()
        
        SKPaymentQueue.default().add(self)
        self.requestProducts()
    }
    
    deinit {
        SKPaymentQueue.default().remove(self)
    }
    
    var canMakePayments: Bool {
        return SKPaymentQueue.canMakePayments()
    }
    
    private func requestProducts() {
        Logger.shared.log("InAppPurchaseManager", "Requesting products")
        let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
        productRequest.delegate = self
        productRequest.start()
        
        self.productRequest = productRequest
        self.lastRequestTimestamp = CFAbsoluteTimeGetCurrent()
    }
    
    public var availableProducts: Signal<[Product], NoError> {
        if self.products.isEmpty {
            if let lastRequestTimestamp, CFAbsoluteTimeGetCurrent() - lastRequestTimestamp > 10.0 {
                Logger.shared.log("InAppPurchaseManager", "No available products, rerequest")
                self.requestProducts()
            }
        }
        return self.productsPromise.get()
    }
    
    public func restorePurchases(completion: @escaping (RestoreState) -> Void) {
        Logger.shared.log("InAppPurchaseManager", "Restoring purchases")
        self.onRestoreCompletion = completion
        
        let paymentQueue = SKPaymentQueue.default()
        paymentQueue.restoreCompletedTransactions()
    }
    
    public func finishAllTransactions() {
        Logger.shared.log("InAppPurchaseManager", "Finishing all transactions")
        
        let paymentQueue = SKPaymentQueue.default()
        let transactions = paymentQueue.transactions
        for transaction in transactions {
            paymentQueue.finishTransaction(transaction)
        }
    }
    
    public func buyProduct(_ product: Product, quantity: Int32 = 1, purpose: AppStoreTransactionPurpose) -> Signal<PurchaseState, PurchaseError> {
        if !self.canMakePayments {
            return .fail(.cantMakePayments)
        }
                
        let accountPeerId: String
        switch self.engine {
        case let .authorized(engine):
            accountPeerId = "\(engine.account.peerId.toInt64())"
        case let .unauthorized(engine):
            accountPeerId = "\(engine.account.id.int64)"
        }
        
        Logger.shared.log("InAppPurchaseManager", "Buying: account \(accountPeerId), product \(product.skProduct.productIdentifier), price \(product.price)")
        
        let purpose = PendingInAppPurchaseState.Purpose(appStorePurpose: purpose)
        
        let payment = SKMutablePayment(product: product.skProduct)
        payment.applicationUsername = accountPeerId
        payment.quantity = Int(quantity)
        SKPaymentQueue.default().add(payment)
        
        let productIdentifier = payment.productIdentifier
        let signal = Signal<PurchaseState, PurchaseError> { subscriber in
            let disposable = MetaDisposable()
            
            self.stateQueue.async {
                let paymentContext = PaymentTransactionContext(purpose: purpose, subscriber: { state in
                    switch state {
                        case let .purchased(transactionId), let .restored(transactionId):
                            if let transactionId = transactionId {
                                subscriber.putNext(.purchased(transactionId: transactionId))
                                subscriber.putCompletion()
                            } else {
                                subscriber.putError(.generic)
                            }
                        case let .failed(error):
                            if let error = error {
                                let mappedError: PurchaseError
                                switch error.code {
                                    case .paymentCancelled:
                                        mappedError = .cancelled
                                    case .cloudServiceNetworkConnectionFailed, .cloudServicePermissionDenied:
                                        mappedError = .network
                                    case .paymentNotAllowed, .clientInvalid:
                                        mappedError = .notAllowed
                                    case .unknown:
                                        if let _ = error.userInfo["tryLater"] {
                                            mappedError = .tryLater
                                        } else {
                                            mappedError = .generic
                                        }
                                    default:
                                        mappedError = .generic
                                }
                                subscriber.putError(mappedError)
                            } else {
                                subscriber.putError(.generic)
                            }
                        case .assignFailed:
                            subscriber.putError(.assignFailed)
                        case .deferred, .purchasing:
                            break
                    }
                })
                self.paymentContexts[productIdentifier] = paymentContext
                
                disposable.set(ActionDisposable { [weak paymentContext] in
                    self.stateQueue.async {
                        if let current = self.paymentContexts[productIdentifier], current === paymentContext {
                            self.paymentContexts.removeValue(forKey: productIdentifier)
                        }
                    }
                })
            }
            
            return disposable
        }
        return signal
    }
    
    public struct ReceiptPurchase: Equatable {
        public let productId: String
        public let transactionId: String
        public let expirationDate: Date
    }
    
    public func getReceiptPurchases() -> [ReceiptPurchase] {
        guard let data = getReceiptData(), let receipt = parseReceipt(data) else {
            return []
        }
        return receipt.purchases.map { ReceiptPurchase(productId: $0.productId, transactionId: $0.transactionId, expirationDate: $0.expirationDate) }
    }
}

extension InAppPurchaseManager: SKProductsRequestDelegate {
    public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        self.productRequest = nil
        
        Queue.mainQueue().async {
            let products = response.products.map { Product(skProduct: $0) }
             
            Logger.shared.log("InAppPurchaseManager", "Received products \(products.map({ $0.skProduct.productIdentifier }).joined(separator: ", "))")
            self.productsPromise.set(.single(products))
        }
    }
}

private func getReceiptData() -> Data? {
    var receiptData: Data?
    if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
        do {
            receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
        } catch {
            Logger.shared.log("InAppPurchaseManager", "Couldn't read receipt data with error: \(error.localizedDescription)")
        }
    }
    return receiptData
}

extension InAppPurchaseManager: SKPaymentTransactionObserver {
    public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        self.stateQueue.async {
            let accountPeerId: String
            switch self.engine {
            case let .authorized(engine):
                accountPeerId = "\(engine.account.peerId.toInt64())"
            case let .unauthorized(engine):
                accountPeerId = "\(engine.account.id.int64)"
            }
            
            let paymentContexts = self.paymentContexts
            
            var transactionsToAssign: [SKPaymentTransaction] = []
            for transaction in transactions {
                if let applicationUsername = transaction.payment.applicationUsername, applicationUsername != accountPeerId {
                    continue
                }
                
                let productIdentifier = transaction.payment.productIdentifier
                let transactionState: TransactionState?
                switch transaction.transactionState {
                    case .purchased:
                        if transaction.payment.productIdentifier.contains(".topup."), let transactionIdentifier = transaction.transactionIdentifier, self.finishedSuccessfulTransactions.contains(transactionIdentifier) {
                            Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") seems to be already reported, ask to try later")
                            transactionState = .failed(error: SKError(SKError.Code.unknown, userInfo: ["tryLater": true]))
                            queue.finishTransaction(transaction)
                        } else {
                            Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
                            transactionState = .purchased(transactionId: transaction.transactionIdentifier)
                            transactionsToAssign.append(transaction)
                        }
                    case .restored:
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "") restroring")
                        let transactionIdentifier = transaction.transactionIdentifier
                        transactionState = .restored(transactionId: transactionIdentifier)
                    case .failed:
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") failed \((transaction.error as? SKError)?.localizedDescription ?? "")")
                        transactionState = .failed(error: transaction.error as? SKError)
                        queue.finishTransaction(transaction)
                    case .purchasing:
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") purchasing")
                        transactionState = .purchasing
                        if let paymentContext = self.paymentContexts[transaction.payment.productIdentifier] {
                            let _ = updatePendingInAppPurchaseState(
                                engine: self.engine,
                                productId: transaction.payment.productIdentifier,
                                content: PendingInAppPurchaseState(
                                    productId: transaction.payment.productIdentifier,
                                    purpose: paymentContext.purpose
                                )
                            ).start()
                        }
                    case .deferred:
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? "") deferred")
                        transactionState = .deferred
                    default:
                        transactionState = nil
                }
                if let transactionState = transactionState {
                    if let context = self.paymentContexts[productIdentifier] {
                        context.subscriber(transactionState)
                    }
                }
            }
            
            if !transactionsToAssign.isEmpty {
                let transactionIds = transactionsToAssign.compactMap({ $0.transactionIdentifier }).joined(separator: ", ")
                Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), sending receipt for transactions [\(transactionIds)]")
                
                guard let transaction = transactionsToAssign.first else {
                    return
                }
                let productIdentifier = transaction.payment.productIdentifier
                
                var completion: Signal<Never, NoError> = .never()
                
                let products = self.availableProducts
                |> filter { products in
                    return !products.isEmpty
                }
                |> take(1)
                
                let product: Signal<InAppPurchaseManager.Product?, NoError> = products
                |> map { products in
                    if let product = products.first(where: { $0.id == productIdentifier }) {
                        return product
                    } else {
                        return nil
                    }
                }
                
                let purpose: Signal<AppStoreTransactionPurpose, NoError>
                if let paymentContext = paymentContexts[productIdentifier] {
                    purpose = product
                    |> map { product in
                        return paymentContext.purpose.appStorePurpose(product: product)
                    }
                } else {
                    purpose = combineLatest(
                        product,
                        pendingInAppPurchaseState(engine: self.engine, productId: productIdentifier)
                    )
                    |> mapToSignal { product, state -> Signal<AppStoreTransactionPurpose, NoError> in
                        if let state {
                            return .single(state.purpose.appStorePurpose(product: product))
                        } else {
                            return .complete()
                        }
                    }
                }
                
                completion = updatePendingInAppPurchaseState(engine: self.engine, productId: productIdentifier, content: nil)
                
                let receiptData = getReceiptData() ?? Data()
#if DEBUG
                self.debugSaveReceipt(receiptData: receiptData)
#endif
                
                for transaction in transactionsToAssign {
                    if let transactionIdentifier = transaction.transactionIdentifier {
                        self.finishedSuccessfulTransactions.insert(transactionIdentifier)
                    }
                }
                
                self.disposableSet.set(
                    (purpose
                    |> castError(AssignAppStoreTransactionError.self)
                    |> mapToSignal { purpose -> Signal<Never, AssignAppStoreTransactionError> in
                        switch self.engine {
                        case let .authorized(engine):
                            return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose)
                        case let .unauthorized(engine):
                            return engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: purpose)
                        }
                    }).start(error: { [weak self] _ in
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] failed to assign")
                        for transaction in transactions {
                            self?.stateQueue.async {
                                if let strongSelf = self, let context = strongSelf.paymentContexts[transaction.payment.productIdentifier] {
                                    context.subscriber(.assignFailed)
                                }
                            }
                            queue.finishTransaction(transaction)
                        }
                    }, completed: {
                        Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transactions [\(transactionIds)] successfully assigned")
                        for transaction in transactions {
                            queue.finishTransaction(transaction)
                        }
                        
                        let _ = completion.start()
                    }),
                    forKey: transactionIds
                )
            }
        }
    }
    
    public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        Queue.mainQueue().async {
            if let onRestoreCompletion = self.onRestoreCompletion {
                Logger.shared.log("InAppPurchaseManager", "Transactions restoration finished")
                self.onRestoreCompletion = nil
                
                if let receiptData = getReceiptData() {
                    let signal: Signal<Never, AssignAppStoreTransactionError>
                    switch self.engine {
                    case let .authorized(engine):
                        signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore)
                    case let .unauthorized(engine):
                        signal = engine.payments.sendAppStoreReceipt(receipt: receiptData, purpose: .restore)
                    }
                    self.disposableSet.set(
                        signal.start(error: { error in
                            Queue.mainQueue().async {
                                if case .serverProvided = error {
                                    onRestoreCompletion(.succeed(true))
                                } else {
                                    onRestoreCompletion(.succeed(false))
                                }
                            }
                        }, completed: {
                            Queue.mainQueue().async {
                                onRestoreCompletion(.succeed(false))
                            }
                            Logger.shared.log("InAppPurchaseManager", "Sent restored receipt")
                        }),
                        forKey: "restore"
                    )
                } else {
                    onRestoreCompletion(.succeed(false))
                }
            }
        }
    }
    
    public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        Queue.mainQueue().async {
            if let onRestoreCompletion = self.onRestoreCompletion {
                Logger.shared.log("InAppPurchaseManager", "Transactions restoration failed with error \((error as? SKError)?.localizedDescription ?? "")")
                onRestoreCompletion(.failed)
                self.onRestoreCompletion = nil
            }
        }
    }
    
    private func debugSaveReceipt(receiptData: Data) {
        guard case let .authorized(engine) = self.engine else {
            return
        }
        let id = Int64.random(in: Int64.min ... Int64.max)
        let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false)
        engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData)

        let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: [])
        let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])

        let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start()
    }
}

private final class PendingInAppPurchaseState: Codable {
    enum CodingKeys: String, CodingKey {
        case productId
        case purpose
        case storeProductId
    }
    
    enum Purpose: Codable {
        enum DecodingError: Error {
            case generic
        }
        
        enum CodingKeys: String, CodingKey {
            case type
            case peer
            case peers
            case boostPeer
            case additionalPeerIds
            case countries
            case onlyNewSubscribers
            case showWinners
            case prizeDescription
            case randomId
            case untilDate
            case stars
            case users
            case text
            case entities
            case restore
            case phoneNumber
            case phoneCodeHash
        }
        
        enum PurposeType: Int32 {
            case subscription
            case upgrade
            case restore
            case gift
            case giftCode
            case giveaway
            case stars
            case starsGift
            case starsGiveaway
            case authCode
        }
        
        case subscription
        case upgrade
        case restore
        case gift(peerId: EnginePeer.Id)
        case giftCode(peerIds: [EnginePeer.Id], boostPeer: EnginePeer.Id?, text: String?, entities: [MessageTextEntity]?)
        case giveaway(boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32)
        case stars(count: Int64, peerId: EnginePeer.Id?)
        case starsGift(peerId: EnginePeer.Id, count: Int64)
        case starsGiveaway(stars: Int64, boostPeer: EnginePeer.Id, additionalPeerIds: [EnginePeer.Id], countries: [String], onlyNewSubscribers: Bool, showWinners: Bool, prizeDescription: String?, randomId: Int64, untilDate: Int32, users: Int32)
        case authCode(restore: Bool, phoneNumber: String, phoneCodeHash: String)
        
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)

            let type = PurposeType(rawValue: try container.decode(Int32.self, forKey: .type))
            switch type {
            case .subscription:
                self = .subscription
            case .upgrade:
                self = .upgrade
            case .restore:
                self = .restore
            case .gift:
                self = .gift(
                    peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer))
                )
            case .giftCode:
                self = .giftCode(
                    peerIds: try container.decode([Int64].self, forKey: .peers).map { EnginePeer.Id($0) },
                    boostPeer: try container.decodeIfPresent(Int64.self, forKey: .boostPeer).flatMap({ EnginePeer.Id($0) }),
                    text: try container.decodeIfPresent(String.self, forKey: .text),
                    entities: try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities)
                )
            case .giveaway:
                self = .giveaway(
                    boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
                    additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) },
                    countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [],
                    onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers),
                    showWinners: try container.decodeIfPresent(Bool.self, forKey: .showWinners) ?? false,
                    prizeDescription: try container.decodeIfPresent(String.self, forKey: .prizeDescription),
                    randomId: try container.decode(Int64.self, forKey: .randomId),
                    untilDate: try container.decode(Int32.self, forKey: .untilDate)
                )
            case .stars:
                self = .stars(
                    count: try container.decode(Int64.self, forKey: .stars),
                    peerId: try container.decodeIfPresent(Int64.self, forKey: .peer).flatMap { EnginePeer.Id($0) }
                )
            case .starsGift:
                self = .starsGift(
                    peerId: EnginePeer.Id(try container.decode(Int64.self, forKey: .peer)),
                    count: try container.decode(Int64.self, forKey: .stars)
                )
            case .starsGiveaway:
                self = .starsGiveaway(
                    stars: try container.decode(Int64.self, forKey: .stars),
                    boostPeer: EnginePeer.Id(try container.decode(Int64.self, forKey: .boostPeer)),
                    additionalPeerIds: try container.decode([Int64].self, forKey: .randomId).map { EnginePeer.Id($0) },
                    countries: try container.decodeIfPresent([String].self, forKey: .countries) ?? [],
                    onlyNewSubscribers: try container.decode(Bool.self, forKey: .onlyNewSubscribers),
                    showWinners: try container.decodeIfPresent(Bool.self, forKey: .showWinners) ?? false,
                    prizeDescription: try container.decodeIfPresent(String.self, forKey: .prizeDescription),
                    randomId: try container.decode(Int64.self, forKey: .randomId),
                    untilDate: try container.decode(Int32.self, forKey: .untilDate),
                    users: try container.decode(Int32.self, forKey: .users)
                )
            case .authCode:
                self = .authCode(
                    restore: try container.decode(Bool.self, forKey: .restore),
                    phoneNumber: try container.decode(String.self, forKey: .phoneNumber),
                    phoneCodeHash: try container.decode(String.self, forKey: .phoneCodeHash)
                )
            default:
                throw DecodingError.generic
            }
        }
        
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            
            switch self {
            case .subscription:
                try container.encode(PurposeType.subscription.rawValue, forKey: .type)
            case .upgrade:
                try container.encode(PurposeType.upgrade.rawValue, forKey: .type)
            case .restore:
                try container.encode(PurposeType.restore.rawValue, forKey: .type)
            case let .gift(peerId):
                try container.encode(PurposeType.gift.rawValue, forKey: .type)
                try container.encode(peerId.toInt64(), forKey: .peer)
            case let .giftCode(peerIds, boostPeer, text, entities):
                try container.encode(PurposeType.giftCode.rawValue, forKey: .type)
                try container.encode(peerIds.map { $0.toInt64() }, forKey: .peers)
                try container.encodeIfPresent(boostPeer?.toInt64(), forKey: .boostPeer)
                try container.encodeIfPresent(text, forKey: .text)
                try container.encodeIfPresent(entities, forKey: .entities)
            case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
                try container.encode(PurposeType.giveaway.rawValue, forKey: .type)
                try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
                try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds)
                try container.encode(countries, forKey: .countries)
                try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers)
                try container.encode(showWinners, forKey: .showWinners)
                try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription)
                try container.encode(randomId, forKey: .randomId)
                try container.encode(untilDate, forKey: .untilDate)
            case let .stars(count, peerId):
                try container.encode(PurposeType.stars.rawValue, forKey: .type)
                try container.encode(count, forKey: .stars)
                try container.encodeIfPresent(peerId?.toInt64(), forKey: .peer)
            case let .starsGift(peerId, count):
                try container.encode(PurposeType.starsGift.rawValue, forKey: .type)
                try container.encode(peerId.toInt64(), forKey: .peer)
                try container.encode(count, forKey: .stars)
            case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users):
                try container.encode(PurposeType.starsGiveaway.rawValue, forKey: .type)
                try container.encode(stars, forKey: .stars)
                try container.encode(boostPeer.toInt64(), forKey: .boostPeer)
                try container.encode(additionalPeerIds.map { $0.toInt64() }, forKey: .additionalPeerIds)
                try container.encode(countries, forKey: .countries)
                try container.encode(onlyNewSubscribers, forKey: .onlyNewSubscribers)
                try container.encode(showWinners, forKey: .showWinners)
                try container.encodeIfPresent(prizeDescription, forKey: .prizeDescription)
                try container.encode(randomId, forKey: .randomId)
                try container.encode(untilDate, forKey: .untilDate)
                try container.encode(users, forKey: .users)
            case let .authCode(restore, phoneNumber, phoneCodeHash):
                try container.encode(PurposeType.authCode.rawValue, forKey: .type)
                try container.encode(restore, forKey: .restore)
                try container.encode(phoneNumber, forKey: .phoneNumber)
                try container.encode(phoneCodeHash, forKey: .phoneCodeHash)
            }
        }
        
        init(appStorePurpose: AppStoreTransactionPurpose) {
            switch appStorePurpose {
            case .subscription:
                self = .subscription
            case .upgrade:
                self = .upgrade
            case .restore:
                self = .restore
            case let .gift(peerId, _, _):
                self = .gift(peerId: peerId)
            case let .giftCode(peerIds, boostPeer, _, _, text, entities):
                self = .giftCode(peerIds: peerIds, boostPeer: boostPeer, text: text, entities: entities)
            case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _):
                self = .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate)
            case let .stars(count, _, _, peerId):
                self = .stars(count: count, peerId: peerId)
            case let .starsGift(peerId, count, _, _):
                self = .starsGift(peerId: peerId, count: count)
            case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, _, _, users):
                self = .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, users: users)
            case let .authCode(restore, phoneNumber, phoneCodeHash, _, _):
                self = .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash)
            }
        }
        
        func appStorePurpose(product: InAppPurchaseManager.Product?) -> AppStoreTransactionPurpose {
            let (currency, amount) = product?.priceCurrencyAndAmount ?? ("", 0)
            switch self {
            case .subscription:
                return .subscription
            case .upgrade:
                return .upgrade
            case .restore:
                return .restore
            case let .gift(peerId):
                return .gift(peerId: peerId, currency: currency, amount: amount)
            case let .giftCode(peerIds, boostPeer, text, entities):
                return .giftCode(peerIds: peerIds, boostPeer: boostPeer, currency: currency, amount: amount, text: text, entities: entities)
            case let .giveaway(boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate):
                return .giveaway(boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount)
            case let .stars(count, peerId):
                return .stars(count: count, currency: currency, amount: amount, peerId: peerId)
            case let .starsGift(peerId, count):
                return .starsGift(peerId: peerId, count: count, currency: currency, amount: amount)
            case let .starsGiveaway(stars, boostPeer, additionalPeerIds, countries, onlyNewSubscribers, showWinners, prizeDescription, randomId, untilDate, users):
                return .starsGiveaway(stars: stars, boostPeer: boostPeer, additionalPeerIds: additionalPeerIds, countries: countries, onlyNewSubscribers: onlyNewSubscribers, showWinners: showWinners, prizeDescription: prizeDescription, randomId: randomId, untilDate: untilDate, currency: currency, amount: amount, users: users)
            case let .authCode(restore, phoneNumber, phoneCodeHash):
                return .authCode(restore: restore, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, currency: currency, amount: amount)
            }
        }
    }
    
    public let productId: String
    public let purpose: Purpose
        
    public init(productId: String, purpose: Purpose) {
        self.productId = productId
        self.purpose = purpose
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.productId = try container.decode(String.self, forKey: .productId)
        self.purpose = try container.decode(Purpose.self, forKey: .purpose)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(self.productId, forKey: .productId)
        try container.encode(self.purpose, forKey: .purpose)
    }
}

private func pendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String) -> Signal<PendingInAppPurchaseState?, NoError> {
    let key = EngineDataBuffer(length: 8)
    key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue))
    
    switch engine {
    case let .authorized(engine):
        return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key))
        |> map { entry -> PendingInAppPurchaseState? in
            return entry?.get(PendingInAppPurchaseState.self)
        }
    case let .unauthorized(engine):
        return engine.itemCache.get(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)
        |> map { entry -> PendingInAppPurchaseState? in
            return entry?.get(PendingInAppPurchaseState.self)
        }
    }
}

private func updatePendingInAppPurchaseState(engine: SomeTelegramEngine, productId: String, content: PendingInAppPurchaseState?) -> Signal<Never, NoError> {
    let key = EngineDataBuffer(length: 8)
    key.setInt64(0, value: Int64(bitPattern: productId.persistentHashValue))
    
    
    switch engine {
    case let .authorized(engine):
        if let content = content {
            return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content)
        } else {
            return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)
        }
    case let .unauthorized(engine):
        if let content = content {
            return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key, item: content)
        } else {
            return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.pendingInAppPurchaseState, id: key)
        }
    }
}
